<ui-plugin name="flow" class="flow">

	<header class="header">

		<div is="is-div" path="?.head.proxyurl" config="show;href a" class="header-special">
			<button class="exec" data-exec="?/copyendpoint" title="@(Copy endpoint to the clipboard)" style="margin-left:10px"><i class="ti ti-link"></i></button>
			<a title="@(Open endpoint)" target="_blank"><i class="ti ti-globe"></i></a>
		</div>

		<div class="pull-right">
			<a href="https://docs.totaljs.com/flow10/" title="@(Documentation: Total.js Platform)" target="_blank" class="special"><i class="ti ti-book"></i></a>
			<button title="@(FlowStream information)" class="exec pull-right" data-exec="?/info"><i class="ti ti-info-circle color"></i></button>
		</div>

		<div is="is-div" path="?.changed" config=".highlight button" class="apply-button">
			<button class="exec apply" data-exec="?/submit"><i class="ti ti-play"></i><span>@(APPLY)</span></button>
		</div>

		<div>
			<button title="@(Pause)" class="exec" data-exec="?/pause" is="is-button" path="?.stats.paused" config=".paused"><i class="ti ti-pause"></i><span></span></button>
			<button title="@(Add)" class="exec green" data-exec="?/add"><i class="ti ti-plus-circle"></i></button>
			<button title="@(Variables)" class="exec" data-exec="?/variables"><i class="ti ti-variables"></i></button>
			<button title="@(Console)" class="exec" data-exec="?/console" is="is-button" path="common.logs.errors.items.length" config=".red"><i class="ti ti-bug"></i></button>
			<!-- <button title="@(Reset)" class="exec hidden" data-exec="?/reset" is="is-button" path="common.logs.errors.items.length" config="show"><i class="ti ti-clean"></i></button> -->
			<button title="@(Search in configuration)" class="exec" data-exec="?/search"><i class="ti ti-file-search"></i></button>
		</div>

		<div>
			<!--
			<button disabled title="@(Undo)" is="is-button" path="?.undo" config="enabled:value && value.length" class="exec" data-exec="?/operation" data-type="undo"><i class="ti ti-undo"></i></button>
			<button disabled title="@(Redo)" is="is-button" path="?.redo" config="enabled:value && value.length" class="exec" data-exec="?/operation" data-type="redo"><i class="ti ti-redo"></i></button>
			-->
			<button title="@(Zoom in)" class="exec" data-exec="?/operation" data-type="in"><i class="ti ti-search-plus"></i></button>
			<button title="@(Zoom out)" class="exec" data-exec="?/operation" data-type="out"><i class="ti ti-search-minus"></i></button>
			<button title="@(Zoom out/Zoom reset)" class="exec" data-exec="?/operation" data-type="out2"><i class="ti ti-search-dot"></i></button>
		</div>

		<div>
			<button title="@(Options)" class="exec" data-exec="?/options"><i class="ti ti-cog"></i></button>
		</div>

		<div style="border-right:0;margin-top:4px">
			<ui-bind path="?.selected" config="show:value&&value.component;template" class="help pull-left mr10">
				<script type="text/html">
					<div>{{ value.Component.name }} ({{ value.component | instancecount }})</div>
					<div>@(ID): <b class="monospace">{{ value.id }}</b></div>
				</script>
			</ui-bind>
		</div>
	</header>
	<div class="header-empty"></div>

	<ui-component name="layout" path="?.layout" config="margin:50">

		<script type="text/plain">
			{
				left: { size: 200, minsize: 150, resize: true },
				bottom: { size: 30 },
				main: {}
			}
		</script>

		<section data-type="left">
			<ui-import config="url:@{#}/panels/flow.html"></ui-import>
		</section>

		<section data-type="main">
			<ui-component name="viewbox" config="parent:auto;visibleX:true;visibleY:true;scrollbar:true;margin:0;scrollbarshadow:0;$id:flowviewbox">
				<ui-component name="flow" path="?.data" config="width:10000;height:10000;markers:0;grid:16;horizontal:1;snapping:0;=curvedlines:common.curvedlines;steplines:1;infopath:?/refreshinfo;outputoffsetY:2;inputoffsetY:2;undopath:?.undo;redopath:?.redo;ondrop:?/drop;contextmenu:?/contextmenu;dblclick:?/settings;onmove:?/onmove;onpause:?/onpause;onconnect:?/onconnect;$assign:Designer;dblclickgroup:?/groupedit;animationlimit:300;animationlimitconnection:3"></ui-component>
			</ui-component>
		</section>

		<section data-type="bottom">
			<div class="stats">
				<div class="realtime" title="@(Total messages)">
					<i class="ti ti-calculator"></i>@(Messages): <ui-bind path="?.stats.messages" config="text;empty;format:0" class="b"></ui-bind>
				</div>
				<div class="realtime" title="@(Pending messages)">
					<i class="ti ti-refresh"></i>@(Pending): <ui-bind class="gray" path="?.stats.pending" config="text;empty;format:0"></ui-bind>
				</div>
				<div class="realtime" title="@(Messages per minute)">
					<i class="ti ti-clock"></i>@(Per minute): <ui-bind class="gray" path="?.stats.mm" config="text;empty;format:0"></ui-bind>
				</div>
				<div class="realtime" title="@(Used nodes)">
					<i class="ti ti-code-branch"></i>@(Nodes): <ui-bind class="gray" path="?.connected" config="text;empty;format:0"></ui-bind>
				</div>
				<div class="realtime" title="@(Memory usage)">
					<i class="ti ti-microchip"></i>@(Memory): <ui-bind class="gray" path="?.stats.memory" config="text:Thelpers.filesize(value||0)"></ui-bind>
				</div>
			</div>
		</section>

	</ui-component>

</ui-plugin>

<script id="template_flowsettings" type="text/html">
	<ui-plugin path="flow.settings.@NAME@" id="@NAME@">
		@BODY@
	</ui-plugin>
</script>

<ui-component name="box" path="common.form" config="if:settings;width:800;visibleY:1;icon:ti ti-cog;submit:flow/settings_submit;$id:flowsettings" class="hidden invisible">
	<nav>
		<ui-bind path="navigator.clipboard" config="show .clipboard">
			<button class="exec" data-exec="flow/readme2" title="@(Readme)"><i class="ti ti-info-circle nmr"></i></button>
			<button class="exec clipboard" data-exec="flow/copyconfig" title="@(Copy config)"><i class="ti ti-copy nmr"></i></button>
			<button class="exec clipboard" data-exec="flow/pasteconfig" title="@(Paste config)"><i class="ti ti-paste nmr"></i></button>
			<button class="exec clipboard" data-exec="flow/resetconfig" title="@(Reset config)"><i class="ti ti-refresh nmr"></i></button>
		</ui-bind>
	</nav>
	<div id="flow_settings"></div>
	<nav>
		<ui-component name="validate" path="settings" config="$id:settingsvalidation">
			<button name="submit"><i class="ti ti-check-circle"></i>@(APPLY)</button>
			<button name="cancel">@(Cancel)</button>
		</ui-component>
	</nav>
</ui-component>

<!-- A temporary repository for components settings -->
<div id="flow_repo" class="hidden"></div>

<ui-component name="importer" path="common.form" config="if:groupform;url:@{#}/forms/group.html"></ui-component>
<ui-component name="importer" path="common.form" config="if:newflow;url:@{#}/forms/new.html"></ui-component>
<ui-component name="importer" path="common.form" config="if:noteform;url:@{#}/forms/note.html"></ui-component>
<ui-component name="importer" path="common.form" config="if:publishform;url:@{#}/forms/publish.html"></ui-component>
<ui-component name="importer" path="common.form" config="if:formendpoint;url:@{#}/forms/endpoint.html"></ui-component>

<script>

	if (!W.flow)
		W.flow = { data: {}, note: {}, config: {}, variables: {}, variables2: {}, status: {}, errors: {}, calls: {}, callbacks: {}, head: {}, regexp: {} };

	Thelpers.instancecount = function(id) {
		var count = 0;
		for (var key in flow.data) {
			var instance = flow.data[key];
			if (instance.component === id)
				count++;
		}
		return count;
	};

	function cleanlogs() {
		if (common.logs.errors.items.length > 50)
			common.logs.errors.items.pop();
	}

	// Reads flow statuses
	SETTER(true, 'websocket/send', { TYPE: 'flow' });

	FIND('websocket', function(com) {
		com.send2 = com.send;
		com.send = function(data) {
			if (data.callback) {
				data.callbackid = GUID(10);
				flow.callbacks[data.callbackid] = { timeout: Date.now(), callback: data.callback };
				data.callback = undefined;
			}
			com.send2(data);
		}
	});

	CACHEPATH('common.curvedlines', '1 year');

	if (common.curvedlines == null)
		common.curvedlines = true;

	PLUGIN('flow', function(exports) {

		var TYPES = { pub: 1, sub: 1, call: 1 };
		var initialized_settings = {};
		var checksums = {};
		var settings_current;
		var settings_data;
		var flowstreamid = common.socket.split('/').trim()[3];
		var init = false;
		var sourcecache = {};

		exports.init = function() {

			ADD({ name: 'websocket', config: 'url:{0};encoder:false'.format(common.socket.replace(/^http/, 'ws').replace(/:/g, '\\:')) });
			var index = common.socket.indexOf('openplatform=');

			if (!(/openplatform=.*?/).test(common.socket))
				$(document).on('click', () => W.parent && W.parent.postMessage(STRINGIFY({ TYPE: 'focus', SOURCE: 'flow', socket: NAV.query.socket })));

		};

		exports.reload = function() {};

		exports.refreshinfo = function(data) {
			var model = exports.model;
			if (model.selected !== data.selected)
				exports.set('selected', data.selected);
			exports.set('info', data);
		};

		exports.call = function(id, data, callback) {
			var callbackid = GUID(10);
			if (typeof(data) === 'function') {
				callback = data;
				data = null;
			}
			flow.calls[callbackid] = callback;
			SETTER('websocket/send', { TYPE: 'call', id: id, data: data, callbackid: callbackid });
		};

		exports.add = function(el) {

			var opt = {};
			opt.element = el;
			opt.items = [];

			opt.items.push({ id: 'create', name: '@(Create component)', icon: 'ti ti-code' });
			opt.items.push({ id: 'opensource', name: '@(Download components)', icon: 'ti ti-download' });
			opt.items.push({ id: 'link', name: '@(Link endpoint)', icon: 'ti ti-network' });
			opt.items.push('-');
			opt.items.push({ id: 'sources', name: '@(TMS sources)', icon: 'ti ti-code-branch' });

			opt.callback = function(item) {

				switch (item.id) {
					case 'link':
						exports.link();
						break;
					case 'create':
						exports.create();
						break;
					case 'opensource':
						exports.components();
						break;
					case 'sources':
						exports.sources();
						break;
				}

			};

			SETTER('menu/show', opt);
		};

		exports.dropfiles = function(files) {
			var arr = [];
			for (var m of files)
				arr.push(m);
			arr.wait(function(file, next) {
				var reader = new FileReader();
				reader.onload = function() {
					install_component(reader.result, function(id, name) {
						id && SETTER('notify/success', '@(Installed: {0})'.format(name));
						reader = null;
						next();
					})
				};
				reader.readAsText(file);
			});
		};

		exports.nodes = function() {
			var id = 'nodes';

			if (common.windows.findItem('id', id)) {
				SETTER('windows/focus', id);
				return;
			}

			PUSH('common.windows', { id: id, cachekey: id, html: '<ui-import config="url:@{#}/forms/nodes.html"></ui-import>', title: '@(Nodes)', actions: { move: true, autosave: true, close: true, maximize: false, minimize: false }, offset: { x: ((WW / 2) - 150) >> 0, y: ((WH / 2) - 200) >> 0, width: 300, height: 400, minwidth: 200, minheight: 300, maxwidth: 500, maxheight: 800 }});
		};

		exports.create = function() {
			common.form && NUL('common.form');
			AJAX('GET /component.txt @showloading', function(response) {
				SET('componenteditor @default', { flowstreamid: flowstreamid, body: response.args({ id: FUNC.makeid('c') }) });
				SET('common.page @hideloading', 'componenteditor');
			});
		};

		exports.link = function() {
			AJAX('GET /endpoint.txt @showloading', function(response) {
				SET('formendpoint @default', { flowstreamid: flowstreamid, body: response });
				SET('common.form @hideloading', 'formendpoint');
			});
		};

		exports.components = function() {
			SET('common.form', 'componentsform');
		};

		exports.find = function(el) {
			var id = ATTRD(el);
			Designer.CMD('flow.find', id);
		};

		exports.clear = function() {
			SETTER('approve/show', '@(Are you sure you want to clear the current Flow?)', '"trash-o" @(Clear)', function() {
				exports.set('data', {});
			});
		};

		exports.reset = function() {
			SETTER('websocket/send', { TYPE: 'reset' });
			SET('common.console', false);
		};

		exports.operation = function(el) {
			var type = el.attrd('type');
			var name = type === 'in' || type === 'out' || type === 'reset' || type === 'out2' ? 'zoom' : type;
			var val;

			if (type === 'out2') {
				var model = exports.model;
				if (model.info.zoom !== 100) {
					type = 'reset';
				} else {
					val = 60;
					type = 'out';
				}
			}

			if (name !== 'zoom')
				type = '';

			Designer.CMD('flow.' + name, type, val);
		};

		exports.copyendpoint = function() {
			var model = exports.model;
			SETTER('clipboard/copy', model.head.proxyurl);
			SETTER('notify/success', '@(Copied)');
		};

		exports.contextmenu = function(e, type, meta) {

			var model = exports.model;
			var info = model.info;
			var item = info.selected;

			if (type === 'map' && info.type !== 'group') {
				var opt = {};
				opt.x = e.pageX;
				opt.y = e.pageY;
				opt.items = [];
				//opt.items.push('');
				opt.items.push({ id: 'paste', name: '@(Paste)', icon: 'ti ti-paste' });
				opt.items.push({ id: 'import', name: '@(Import)', icon: 'ti ti-download' });
				opt.callback = function(opt) {
					Designer.op.unselect();
					exports.paste_components({ x: e.offsetX, y: e.offsetY });
				};
				SETTER('menu/show', opt);
				return;
			}

			if (!item)
				return;

			var count = Designer.selected().components.length;
			var single = count === 1;

			type = info.type;
			var component = item.isconnection ? null : flow.components.findItem('id', item.component);
			var meta = (component ? component.meta : null) || EMPTYOBJECT;

			var opt = {};
			opt.x = e.pageX;
			opt.y = e.pageY;
			opt.items = [];

			if (component) {

				if (single) {
					opt.items.push(component.name + (component.schema ? ' / <b>{schema}</b>'.arg(component) : ''));
					component.readme && opt.items.push({ id: 'readme', name: '@(Read information)', icon: 'ti ti-info-circle blue' });
					opt.items.push({ id: 'note', name: '@(Set a note)', icon: 'ti ti-file' });
				} else {
					opt.items.push(count + ' @(components selected)');
				}

				if (!TYPES[component.type]) {

					var meta = component.meta || EMPTYOBJECT;

					if (component.settings && single) {
						opt.items.push({ id: 'settings', name: '@(Configure)', icon: 'ti ti-wrench', classname: 'b' });
						opt.items.push('-');
					}

					if (!meta.readonly) {
						opt.items.push({ id: 'clone', name: '@(Clone)', icon: 'ti ti-clone' });
						single && opt.items.push({ id: 'edit', name: '@(Edit)', icon: 'ti ti-laptop-code' });
						opt.items.push({ id: 'exportcomponents', name: '@(Copy)', icon: 'ti ti-copy' });
						single && opt.items.push({ id: 'download', name: '@(Download)', icon: 'ti ti-save' });
						single && opt.items.push({ id: 'publish', name: '@(Publish to the community)', icon: 'ti ti-upload' });
						if (meta.remove !== false)
							opt.items.push('-');
					}
				}
			} else if (info.type === 'group') {
				opt.items.push(item.name);
				opt.items.push({ id: 'settings', name: '@(Configure)', icon: 'ti ti-wrench', classname: 'b' });
			} else if (info.type === 'connection' && flow.head.total > 4060) {
				var selector = '.conn__' + item.fromid + '__' + item.toid + '__' + item.fromindex + '__' + item.toindex;
				var colors = ['E73323', 'EC8632', 'FFFD54', '68B25B', '7CFBFD', '4285F4', 'E73CF7', 'E8357E', '73197B', '91683C'];
				var submenu = [{ id: 'color', color: '', name: '@(None)' }];
				var selectedcolor = $(selector).attrd('color');
				for (var m of colors) {
					var color = '#' + m;
					submenu.push({ id: 'color', selected: color === selectedcolor, color: color, name: '<div style="height:20px;background-color:#{0};border-radius:var(--radius);width:100px"></div>'.format(m) });
				}
				opt.items.push({ name: '@(Color)', icon: 'ti ti-paint-brush colorize', children: submenu, classname: 'b' });
			}

			if (!component || meta.remove !== false)
				opt.items.push({ id: 'remove', name: '@(Remove)', icon: 'ti ti-remove red' });

			if (opt.items.last() === '-')
				opt.items.pop();

			opt.callback = function(opt) {
				switch (opt.id) {

					case 'readme':
						exports.scope();
						exports.readme(item.component);
						break;

					case 'copy':
					case 'copy2':
					case 'download':
						SETTER('websocket/send', { TYPE: 'component_read', id: item.component, callback: ASETTER('message/response', function(response) {

							if (opt.id === 'download') {
								SETTER('filesaver/save', component.name + '.html', response.data);
								return;
							}

							SETTER('clipboard/copy', opt.id === 'copy2' ? response.data : ENCRYPT({ body: response.data }, common.secret, 'component'));
							SETTER('notify/success', '@(The component has been copied into the clipboard)');
						})});
						break;

					case 'note':

						var data = {};
						data.note = flow.note[item.id] || '';
						data.callback = function(value) {
							if (item.connected) {
								SETTER('websocket/send', { TYPE: 'note', id: item.id, data: value });
							} else {
								item.note = value;
								SET('flow.note.' + item.id, value);
							}
						};

						SET('noteform @reset', data);
						SET('common.form', 'noteform');
						break;

					case 'settings':
						if (info.type === 'group')
							exports.groupedit(item);
						else
							exports.settings(item);
						break;

					case 'edit':
						SETTER('websocket/send', { TYPE: 'component_read', id: item.component, callback: ASETTER('message/response', function(response) {
							response.body = response.data;
							response.data = null;
							SET('componenteditor @reset', response);
							SET('common.page @hideloading', 'componenteditor');
						})});
						break;

					case 'remove':

						Designer.CMD('flow.selected.clear');

						if (type === 'group')
							SETTER('websocket/send', { TYPE: 'groups', data: flow.data.groups });
						else
							SET('flow.changed', true);

						break;
					case 'clone':
						exports.clone();
						break;
					case 'copycomponents':
						exports.copy_components();
						break;
					case 'exportcomponents':
						exports.export_components();
						break;
					case 'color':
						(function() {
							var conn = info.selected;
							var selector = '.conn__' + conn.fromid + '__' + conn.toid + '__' + conn.fromindex + '__' + conn.toindex;
							var instance = flow.data[conn.fromid];
							if (instance) {
								var arr = instance.connections[conn.fromindex];
								if (arr) {
									for (var item of arr) {
										if (item.id === conn.toid && item.index === conn.toindex)
											item.color = opt.color ? opt.color : undefined;
									}
								}
								SET('flow.changed', true);
							}
							$(selector).attrd('color', opt.color).css({ stroke: opt.color });
						})();
						break;
					case 'publish':
						SETTER('websocket/send', { TYPE: 'component_read', id: item.component, callback: ASETTER('message/response', function(response) {
							var data = {};
							data.type = 'component';
							data.name = item.name;
							data.email = CACHE('email') || '@';
							data.data = response.data;
							SET('publishform @reset', data);
							SET('common.form', 'publishform');
						})});
						break;
				}
			};
			SETTER('menu/show', opt);
		};

		exports.settings = function(instance, e) {

			if (e && e.target && (e.target.tagName === 'BUTTON' || e.target.tagName === 'A'))
				return;

			if (instance instanceof jQuery) {
				instance = flow.data[ATTRD(instance)];
				if (!instance)
					return;
			}

			var rendersettings = function(response) {

				var component = flow.components.findItem('id', instance.component);
				if (!component || !component.settings)
					return SETTER('notify/warning', '@(No settings available)');

				var id = 'settings_f' + component.id;
				var dom = $('#flow_settings')[0];
				var domrepo = $('#flow_repo')[0];
				var domsettings = $('#' + id)[0];
				var move = false;

				if (!domsettings && initialized_settings[id])
					delete initialized_settings[id];

				if (dom.children[0] && dom.children[0] !== domsettings) {
					domrepo.appendChild(dom.children[0]);
					move = true;
				} else if (!dom.children[0])
					move = true;

				flow.settingsid = component.id;
				flow.settingsinstanceid = instance.id;
				RECONFIGURE('#flowsettings', { title: '@(Configuration): ' + component.name, width: (component.meta ? component.meta.settingswidth : 0) || 800 });

				var name = component.name.slug().replace(/\-/g, '');

				if (initialized_settings[id]) {
					move && dom.appendChild(domsettings);
				} else {
					initialized_settings[id] = 1;
					var html = $('#template_flowsettings').html();
					var regcls = new RegExp('CLA' + 'SS', 'g');
					$('#flow_settings').aclass('invisible').append(html.replace(/@NAME@/g, id).replace(/@CONFIG@/g, '?').replace(/@BODY@/g, component.settings.replace(regcls, 'f-' + name)));
					COMPILE();
				}

				var config = response;
				var path = 'flow.settings.' + id;
				var touchinstance = flowinstances.instances[instance.id];
				var eventdata = { id: instance.id, name: name, path: path, config: config, component: component, instance: touchinstance };

				eventdata.call = function(data, callback, globalcall) {

					if (typeof(data) === 'function') {
						globalcall = callback;
						callback = data;
						data = null;
					}

					exports.call(globalcall ? ('@' + this.component.id) : this.id, data, callback);
				};

				W.flowinstances.exec(instance.id, 'settings', eventdata);
				EMIT('configure_' + name, eventdata);
				EMIT('configure_read_' + name, eventdata);
				settings_data = eventdata;
				settings_current = instance;
				$('#flow_settings').rclass('invisible', 1000);
				SETTER('#settingsvalidation/setPath', path);
				SET('flow.settings.{0} @default'.format(id), config);
				SET('common.form', 'settings');
			};

			if (flow.config[instance.id])
				rendersettings(CLONE(flow.config[instance.id]));
			else
				SETTER('websocket/send', { TYPE: 'config', id: instance.id, callback: response => rendersettings(response.data) });
		};

		exports.settings_submit = function() {
			var model = GET(FIND('#settingsvalidation').path + ' @clone');
			settings_data.config = model;
			EMIT('configure_save_' + settings_data.name, settings_data);
			settings_current.config = settings_data.config;
			FUNC.reconfigure(settings_current.id, model);
			NUL('common.form');
		};

		exports.exportasbase64 = function() {
			SETTER('websocket/send', { TYPE: 'export', callback: ASETTER('message/response', function(response) {
				SETTER('clipboard/copy', btoa(encodeURIComponent(STRINGIFY(response.data))));
				SETTER('notify/success', '@(The FlowStream has been copied into the clipboard as base64 format)');
			})});
		};

		exports.exportascomponent = function() {
			SETTER('approve/show', '@(Are you sure you want to create a component from this FlowStream?)', '"ti ti-file-export" @(Export)', function() {
				SETTER('websocket/send', { TYPE: 'export', callback: ASETTER('message/response', function(response) {

					var flow = response.data;
					var inputs = [];
					var outputs = [];

					delete flow.origin;

					for (var key in flow.components) {
						var tmp = flow.components[key];
						var index = tmp.indexOf('<scr' + 'ipt total>');
						if (index !== -1)
							flow.components[key] = tmp.substring(index, tmp.indexOf('</sc' + 'ript>', index + 20) + 9);
					}

					for (var key in flow.design) {
						if (key === 'groups' || key === 'paused' || key === 'tabs')
							continue;
						var fi = flow.design[key];
						var template = fi.template || fi.meta; // due to backward compatibility
						if (template) {
							if (template.type === 'input')
								inputs.push({ id: fi.id, name: fi.config.name || 'Input' });
							else if (template.type === 'output')
								outputs.push({ id: fi.id, name: fi.config.name || 'Output' });
						}
					}

					var make = `function(instance, config) {

		var stream = null;
		var schema = CLONE(instance.module.flowstreamschema);

		schema.id = instance.id + GUID(5);
		schema.variables = config;
		schema.iscomponent = true;

		var output = function(fid, data, tfsid, tid, reference) {
			instance.send(fid, data);
		};

		instance.newflowstream(schema, {0}, function(err, worker) {
			stream = worker;
			if (stream)
				stream.output = output;
		});

		instance.message = function($) {
			if (stream) {
				var reference = null;
				stream.input(null, null, $.input, $.data, reference);
			}
			$.destroy();
		};

		instance.configure = function() {
			stream && stream.variables(config);
		};

		instance.close = function() {
			stream && stream.destroy();
		};

	};`.format(flow.worker ? ('\'' + flow.worker + '\'') : 'false');

						var builder = [];
						builder.push('<scri' + 'pt total>');
						builder.push('');
						builder.push('\t// This is exported component, copy the source code and put it into another Flow');
						builder.push('');
						builder.push('\texports.name = \'{0}\';'.format(flow.name));
						builder.push('\texports.icon = \'{0}\';'.format(flow.icon));
						builder.push('\texports.group = \'Encapsulated\';');
						builder.push('\texports.reference = \'{0}\';'.format(flow.reference));
						builder.push('\texports.config = {0};'.format(STRINGIFY(flow.variables)));
						builder.push('\texports.inputs = {0};'.format(inputs.length ? STRINGIFY(inputs).replace(/"/g, '\'') : 'null'));
						builder.push('\texports.outputs = {0};'.format(outputs.length ? STRINGIFY(outputs).replace(/"/g, '\'') : 'null'));
						builder.push('\texports.version = \'{0}\';'.format(flow.version));
						builder.push('\texports.flowstreamschema = decodeURIComponent(Buffer.from(\'{0}\', \'base64\').toString(\'utf8\')).parseJSON(true);'.format(btoa(encodeURIComponent(STRINGIFY(flow)))));
						builder.push('');
						builder.push('\texports.make = ' + make);
						builder.push('');
						builder.push('</sc' + 'ript>');
						builder.push('');
						builder.push(`<body>
	<header>
		<i class="{0}"></i>{1}
	</header>
</body>`.format(flow.icon, flow.name));
						builder.push('\n<readme>\n' + flow.readme + '\n</readme>');

						SET('componenteditor @default', { flowstreamid: flowstreamid, body: builder.join('\n') });
						SET('common.page', 'componenteditor');
				})});
			});
		};

		exports.readme2 = function() {
			exports.readme(flow.selected.component);
		};

		exports.readme = function(el) {
			var id = ATTRD(el);
			var model = exports.model;
			var item = model.components.findItem('id', id);
			if (item && item.readme)
				FUNC.readme(item.name, item.readme);
		};

		var checkio = function(a, b) {
			if (!a)
				return;
			if (!b)
				return a;
			if (a.length !== b.length)
				return a;
			for (var i = 0; i < a.length; i++) {
				if (a[i].id !== b[i].id || a[i].name !== b[i].name)
					return a;
			}
		};

		exports.submit = function() {

			SETTER('approve/show', '@(Are you sure you want to save and apply Flow?)', '"ti ti-check-circle" @(Continue)', function() {

				if (BLOCKED('save', 2000))
					return;

				var model = flow.data;
				var output = {};
				var keys = Object.keys(model);

				for (var i = 0; i < keys.length; i++) {
					var key = keys[i];
					var com = model[key];

					if (key === 'paused' || key === 'groups' || key === 'tabs') {
						output[key] = com;
						continue;
					}

					var item = {};

					item.id = com.id;
					item.connections = com.connections;
					item.config = com.config;
					item.component = com.component;

					// Check if the component exists
					var c = flow.components.findItem('id', com.component);
					if (!c)
						continue;

					if (com.outputs)
						item.outputs = checkio(com.outputs, c.outputs);

					if (com.inputs)
						item.inputs = checkio(com.inputs, c.inputs);

					item.x = com.x;
					item.y = com.y;
					item.note = com.note;
					item.tab = com.tab;
					item.connected = true;
					com.changes = null;
					com.connected = true;
					output[item.id] = item;
				}

				$('.isnewbie').rclass('isnewbie');
				SET('?.changed', false);
				SETTER('websocket/send', { TYPE: 'save', data: output });
				Designer.CMD('flow.reset');
			});

		};

		exports.clone = function() {

			var model = exports.model;
			var selected = Designer.selected();
			var add = [];
			var cloned = {};
			var now = Date.now();

			for (var item of selected.components) {
				var instance = model.data[item.id];
				var newbie = {};
				newbie.id = 'i' + now.toString(36);
				newbie.x = instance.x + 50;
				newbie.y = instance.y + 50;
				newbie.name = instance.name;
				newbie.Component = instance.Component;
				newbie.component = instance.component;
				newbie.config = CLONE(instance.config);
				newbie.connected = false;
				// newbie.connections = CLONE(instance.connections);
				newbie.connections = {};
				newbie.name = instance.name.slug().replace(/\-/g, '');
				newbie.html = instance.html.replace(new RegExp(instance.id, 'g'), newbie.id);
				newbie.$outputs = instance.$outputs;
				newbie.outputs = instance.outputs ? CLONE(instance.outputs) : undefined;
				newbie.$inputs = instance.$inputs;
				newbie.inputs = instance.inputs ? CLONE(instance.inputs) : undefined;
				newbie.template = instance.template;
				newbie.onmake = onmake;
				newbie.onremove = onremove;
				newbie.tab = instance.tab;
				cloned[instance.id] = newbie.id;
				add.push(newbie);
				SET('flow.note.' + newbie.id, model.note[item.id]);
				SET('flow.config.' + newbie.id, newbie.config);
				now++;
			}

			for (var item of add) {
				for (var key in item.connections) {
					for (var output of item.connections[key]) {
						if (cloned[output.id])
							output.id = cloned[output.id];
					}
				}
				model.data[item.id] = item;
			}

			Designer.find('.component-focused').rclass('component-focused');
			Designer.op.unselect();

			// UPD('?.data', 'update');
			SET('?.changed', true);

			for (var item of add)
				Designer.CMD('flow.components.add', item);

			setTimeout(function(add) {
				for (var item of add)
					Designer.op.select(item.id, true);
			}, 500, add);
		};

		exports.search = function() {
			var id = 'search';

			if (common.windows.findItem('id', id)) {
				SETTER('windows/focus', id);
				return;
			}

			PUSH('common.windows', { id: id, cachekey: id, html: '<ui-import config="url:@{#}/forms/search.html"></ui-import>', title: '@(Search in configuration)', actions: { move: true, autosave: true, close: true, maximize: false, minimize: false }, offset: { x: ((WW / 2) - 250) >> 0, y: ((WH / 2) - 300) >> 0, width: 500, height: 600, minwidth: 200, minheight: 300, maxwidth: 500, maxheight: 800 }});

		};

		exports.sources = function() {
			SET('common.form', 'sourcesform');
		};

		exports.drop = function(e, grid) {

			var id = ATTRD(e.el);
			var offsetX = e.clickX || 0;
			var offsetY = e.clickY || 0;

			if (id === 'group') {
				var obj = {};
				obj.id = 'g' + Date.now().toString(36);
				obj.x = e.offsetX - offsetX;
				obj.y = e.offsetY - offsetY;
				obj.width = 400;
				obj.height = 400;
				obj.name = '@(Group)';
				obj.tab = flow.tab;
				Designer.CMD('flow.groups.add', obj);
				SETTER('websocket/send', { TYPE: 'groups', data: flow.data.groups });
			} else {
				var com = flow.components.findItem('id', id);
				var obj = {};

				if (com.meta && com.meta.singleton) {

					// check
					var cancel = (function() {
						for (var key in flow.data) {
							var f = flow.data[key];
							if (f.component === com.id)
								return true;
						}
					})();

					if (cancel) {
						SETTER('message/warning', '@(The <b>"{0}"</b> component is already in use)'.format(com.name.encode()));
						return;
					}

				}

				obj.id = 'i' + Date.now().toString(36);
				obj.Component = com;
				obj.component = id;
				obj.x = e.offsetX - offsetX;
				obj.y = e.offsetY - offsetY;
				obj.name = com.name.slug().replace(/\-/g, '');
				obj.outputs = com.outputs ? CLONE(com.outputs) : null;
				obj.inputs = com.inputs ? CLONE(com.inputs) : null;
				obj.config = CLONE(com.config);
				obj.html = '<figure>' + com.html.format(obj.id) + '</figure>';
				obj.template = com.template ? Tangular.compile(com.template) : null;
				obj.onmake = onmake;
				obj.onremove = onremove;
				obj.tab = flow.tab;
				SET('?.changed', true);
				SET('?.config.' + obj.id, obj.config);
				Designer.CMD('flow.components.add', obj);
			}

		};

		var loadflow = function(response) {

			var uninitializedcount = 0;
			var count = 0;
			var blacklist = { tabs: 1, paused: 1, groups: 1 };
			var instances = [];

			exports.set('ready', false);

			for (var key in response) {
				if (key !== 'paused' && key !== 'groups' && key !== 'tabs')
					response[key].connected = true;
			}

			for (var key in flow.data) {
				if (!blacklist[key]) {
					var instance = flow.data[key];
					if (instance.changes && instance.changes.newbie) {
						var item = {};
						item.id = key;
						item.chnages = instance.changes;
						item.connections = instance.connections;
						item.config = instance.config;
						item.component = instance.component;
						item.x = instance.x;
						item.y = instance.y;
						item.note = instance.note;
						item.tab = instance.tab;
						item.connected = false;
						response[key] = item;
						uninitializedcount++;
					}
				}
			}

			for (var key in response) {

				if (blacklist[key])
					continue;

				var obj = response[key];
				var com = flow.components.findItem('id', obj.component);

				if (com == null) {
					// component not found
					WARN('Component "{0}" not found'.format(obj.component));
					delete response[key];
					continue;
				}

				obj.Component = com;
				obj.name = com.name.slug().replace(/\-/g, '');
				obj.html = '<figure>' + com.html.format(key) + '</figure>';
				obj.$outputs = !!obj.outputs;
				obj.outputs = obj.outputs || (com.outputs ? CLONE(com.outputs) : null);
				obj.$inputs = !!obj.outputs;
				obj.inputs = obj.inputs || (com.inputs ? CLONE(com.inputs) : null);
				obj.template = com.template ? Tangular.compile(com.template) : null;
				obj.onmake = onmake;
				obj.onremove = onremove;

				if (obj.connected !== false) {
					obj.connected = true;
					count++;
				}

				flow.note[obj.id] = obj.note;
				flow.config[obj.id] = obj.config;
				instances.push(obj);
			}

			exports.scope();
			SET('?.connected', count);
			SET('?.data', response);
			SET('?.changed', uninitializedcount > 0);
			SET('?.ready', true, 500);
			SET('?.instances', instances);
			Designer.CMD('flow.refresh');
			setTimeout(() => W.flowinstances.exec('*', 'redraw'), 200);
		};

		var onremove = function() {
			W.flowinstances.remove(this.id);
			var index = flow.instances.findIndex('id', this.id);
			if (index !== -1)
				flow.instances.splice(index, 1);
		};

		var onmake = function(el) {

			var self = this;
			var footer = el.find('footer');
			var content;
			if (footer.length) {
				var area = footer.closest('.area');
				var span = area.find('.meta').find('span');
				area[0].appendChild(footer[0]);
				area.parent();
				content = area.find('.content');
			} else
				content = el.find('.content');

			content.append('<summ' + 'ary class="hidden" path="flow.note.' + self.id + '" config="show;html div:Thelpers.markdown(value, { element: el })"><span class="towindow exec" data-exec="?/notewindow"><i class="ti ti-window-alt"></i></span><span>@(Note)</span><div class="noscrollbar selectable markdown-tiny"></div></summ' + 'ary>');
			var target = el.closest('.component').attr('path', 'flow.errors.' + self.id).attr('config', '.iserror:value&&value.length;title:value?value[0].error\:\'\'').tclass('isnewbie', !self.connected);

			NEWUIBIND(content.find('> summary'));
			NEWUIBIND(target);

			var com = flow.components.findItem('id', self.component);
			if (com) {

				el.aclass(com.groupclass);

				if (com.type === 'pub' || com.type === 'sub' || com.type === 'call' || com.type === 'input2' || com.type === 'output2' || com.type === 'input' || com.type === 'output') {
					var span = el.find('.meta').find('span');
					if (span.length)
						span.css('background-color', Thelpers.color(span.html()));
					var arr = el.find('.schema span');
					var type = com.type === 'input2' || com.type === 'input' ? 'pub' : com.type === 'output2' || com.type === 'output' ? 'sub' : com.type;
					for (var i = 0; i < arr.length; i++)
						$(arr[i]).css('background-color', Thelpers.color(arr[i].innerHTML));
					target.aclass('tms-' + type);
				}

				var meta = com.meta || EMPTYOBJECT;
				el.tclass('transparent', meta.hidden ? true : false).tclass('singleton', meta.singleton ? true : false);
				target.aclass('f-' + self.name);
			}

			W.flowinstances.init(self.name, self, el);
		};

		var replacekeywords = function(str, keywords) {

			var allowed = [' ', '\'', '"', '.', '>', '<', '=', '+', '-', ',', ';', '!', '\n', '\t', ':', ' ', '_'];

			for (var key in keywords) {

				var reg = flow.regexp[key];

				if (!reg)
					reg = flow.regexp[key] = new RegExp('\\$' + key, 'g');

				str = str.replace(reg, keywords[key]);

				var index = 0;
				while (true) {
					index = str.indexOf(key, index);
					if (index === -1)
						break;
					var b = str.charAt(index - 1);
					var e = str.charAt(index + key.length);
					if ((!b || allowed.includes(b)) && (!e || allowed.includes(e))) {
						str = str.substring(0, index) + keywords[key] + str.substring(index + key.length);
						index += keywords[key].length;
					} else
						index += key.length;
				}
			}

			return str;
		};

		W.loadcomponenterror = function(e) {
			var msg = (new Date()).format('[ts]') + ' - <b>{0} component</b>: failed client-side code execution - '.format(W.flowinstances.current) + Thelpers.encode(e + '');
			var a = { type: 'error', body: msg };
			PUSH('^common.logs.errors.items', a);
			cleanlogs();
			W.console && W.console.error('Component: "' + com.name + '"', e);
			if (!common.console)
				SET('common.console', true);
		};

		var loadcomponents = function(response) {

			var js = [];
			var css = [];
			var groups = {};
			var checksums_tmp = {};
			var colors = {};
			var clsname = 'CLA' + 'SS';

			for (var i = 0; i < response.length; i++) {

				var com = response[i];
				var name = com.name.slug().replace(/\-/g, '');

				checksums_tmp[com.id] = HASH(com.html);
				com.html = replacekeywords(com.html || '', { UID: '{0}', ID: com.id, ICON: com.icon, NAME: com.name, TOUCH: 'flowinstances.instances.{0}', STATUS: 'flow.status.{0}', CONFIG: 'flow.config.{0}', [clsname]: 'f-' + name });

				if (com.js) {
					var tmp = replacekeywords(com.js, { ID: com.id, [clsname]: 'f-' + name, ICON: com.icon, NAME: com.name });
					js.push('W.flowinstances.current=\'' + name + '\';W.flowinstances.checksum=\'' + HASH(tmp).toString(36) + '\';try{' + tmp + '}catch(e){W.loadcomponenterror(e)}');
				}

				com.css && css.push(replacekeywords(com.css, { [clsname]: 'f-' + name }));
				var g = com.group || 'Common';

				com.groupclass = 'group_' + HASH(g).toString(36);

				if (!colors[com.groupclass])
					colors[com.groupclass] = Thelpers.color(g);

				if (com.meta && com.meta.secret)
					continue;

				if (groups[g])
					groups[g].push(com);
				else
					groups[g] = [com];
			}

			if (js.length) {
				new Function(js.join('\n'))();
				W.flowinstances.current = '';
			}

			var arr = [];
			for (var key in groups) {
				groups[key].quicksort('name');
				arr.push({ name: key, color: Thelpers.color(key), items: groups[key] });
			}

			for (var key in colors)
				css.push('.' + key + ' header i{background:' + colors[key] + '}');

			arr.quicksort('name');
			CSS(css.join(''), 'flowcomponents');

			exports.scope();
			SET('?.components', response);

			exports.scope();
			SET('?.components2', arr);

			var model = exports.model;
			if (model.data) {
				var rebuild = false;
				for (var key in model.data) {
					var instance = model.data[key];
					var com = response.findItem('id', instance.component);
					if (com) {
						var a = checksums_tmp[com.id];
						var b = checksums[com.id];
						var parent = instance.element ? instance.element.closest('.component') : null;

						if (a !== b) {
							instance.html = com.html.format(key);
							rebuild = true;
						}

						if (parent) {
							parent.rclass('f-' + instance.name);
							instance.name = com.name.slug().replace(/\-/g, '');
							parent.aclass('f-' + instance.name);
						}
					} else {
						rebuild = true;
						delete model.data[key];
					}
				}
			}

			checksums = checksums_tmp;

			if (rebuild) {
				UPD('?.data', 'update');
				setTimeout(ASETTER('websocket/send', { TYPE: 'refresh' }), 1000);
			}

			if (!init) {
				init = true;
				if (!response.length)
					SET('common.form', 'newflow');
			}

		};

		exports.pause = function() {
			var model = exports.model;
			var is = model.stats.paused;
			SETTER('loading/show');
			SETTER('websocket/send', { TYPE: 'pause', is: is === true ? false : true });
			SETTER('loading/hide', 3000);
		};

		exports.onconnect = function(meta) {
			if (flow.ready && !meta.init)
				SET('flow.changed', true);
		};

		exports.onpause = function(key, is) {
			var id = key.split('__')[1];
			if (flow.data[id] && (!flow.data[id].changes || !flow.data[id].changes.newbie))
				SETTER('websocket/send', { TYPE: 'pause', id: key, is: is });
		};

		exports.onmove = function(el, item, type) {
			if (type === 'group') {
				SETTER('websocket/send', { TYPE: 'groups', data: flow.data.groups });
			} else {
				item.connected && SETTER('websocket/send', { TYPE: 'move', id: item.id, data: { x: item.x, y: item.y }});
				W.flowinstances.exec(item.id, 'move', { x: item.x, y: item.y });
			}
		};

		exports.groupedit = function(item) {
			SET('groupform @reset', CLONE(item));
			SET('common.form', 'groupform');
		};

		exports.options = function(el) {

			var model = exports.model;
			var opt = {};
			opt.element = el;
			opt.items = [];
			opt.items.push({ id: 'nodes', name: '@(Active nodes)', icon: 'ti ti-list' });
			opt.items.push({ id: 'search', name: '@(Search in configuration)', icon: 'ti ti-file-search' });
			opt.items.push({ id: 'version', name: '@(Version)', icon: 'ti ti-info-circle' });
			opt.items.push('-');
			opt.items.push({ id: 'download', name: '@(Download)', icon: 'ti ti-download' });
			opt.items.push({ id: 'export', name: '@(Export)', icon: 'ti ti-file-export' });
			opt.items.push({ id: 'exportascomponent', name: '@(Export to component)', icon: 'ti ti-plug' });
			opt.items.push({ id: 'exportasbase64', name: '@(Export to BASE64)', icon: 'ti ti-plug' });
			opt.items.push({ id: 'importascomponent', name: '@(Import component)', icon: 'ti ti-paste' });
			opt.items.push('-');
			opt.items.push({ id: 'curvedlines', name: '@(Curved lines)', icon: 'ti ti-bezier-curve', selected: common.curvedlines == true });
			opt.items.push({ id: 'reset_stats', name: '@(Reset stats)', icon: 'ti ti-chart-line' });
			opt.items.push('-');

			if (model.head.worker)
				opt.items.push({ id: 'restart', name: '@(Restart worker)', icon: 'ti ti-sync' });

			opt.items.push({ id: 'clearunused', name: '@(Clear unused components)', icon: 'ti ti-recycle' });
			opt.items.push({ id: 'clear', name: '@(Clear)', icon: 'ti ti-remove red' });

			opt.callback = function(item) {

				switch (item.id) {
					case 'clearunused':

						var arr = [];
						var com = {};
						var names = [];
						for (var key in flow.data)
							com[flow.data[key].component] = 1;

						for (var m of flow.components) {
							if (!com[m.id]) {
								names.push(m.name);
								arr.push(m.id);
							}
						}

						if (!names.length) {
							SETTER('notify/success', '@(Good job, nothing to clear).');
							return;
						}

						SETTER('approve/show', '@(Are you sure you want to clear all unused components ({0})?)'.format(names.join(', ')), '"ti ti-remove" @(Remove)', function() {
							arr.wait(function(id, next) {
								SETTER('websocket/send', { TYPE: 'component_remove', id: id });
								setTimeout(next, 5);
							}, ASETTER('notify/success', '@(Done, all unused components have been removed).'));
						});
						break;
					case 'version':
						SET('common.form', 'versionform');
						break;
					case 'nodes':
					case 'search':
					case 'exportasbase64':
					case 'exportascomponent':
					case 'importascomponent':
					case 'clear':
						exports[item.id]();
						break;
					case 'curvedlines':
						TOGGLE('common.curvedlines');
						break;
					case 'reset_stats':
						SETTER('websocket/send', { TYPE: 'reset_stats' });
						break;
					case 'export':
					case 'download':
						SETTER('websocket/send', { TYPE: 'export', callback: ERROR(function(response) {
							delete response.data.origin;
							var data = JSON.stringify(response.data, null, '\t');
							if (item.id === 'download') {
								SETTER('filesaver/save', flow.head.name + '.json', data, 'json');
							} else {
								SETTER('clipboard/copy', data);
								SETTER('notify/success', '@(The FlowStream has been copied into the clipboard)');
							}
						})});
						break;
					case 'restart':
						SETTER('approve/show', '@(Are you sure you want to restart this FlowStream process?)', '"ti ti-sync" @(Restart)', ASETTER('websocket/send', { TYPE: 'restart' }));
						break;
				}

			};

			SETTER('menu/show', opt);
		};

		exports.importascomponent = function() {
			navigator.clipboard.readText().then(function(text) {

				var meta = FUNC.checkhtml(text);
				if (!meta) {
					SETTER('notify/warning', '@(Invalid data in the clipboard)');
					return;
				}

				var id = meta.id || FUNC.makeid('c');
				var body = FUNC.rtrim(text);

				SETTER('websocket/send @showloading', { TYPE: 'component_save', id: id, data: body, callback: function(){
					SETTER('loading/hide', 1000);
					SETTER('notify/success', '@(The component has been successfuly imported)');
				}});

			}).catch(err => console.error(err));
		};

		exports.refreshvariables = function() {
			Designer.CMD('flow.refresh');
			var keys = {};
			var arr = flow.variables ? Object.keys(flow.variables) : EMPTYARRAY;
			for (var key of arr)
				keys[key] = 1;
			arr = flow.variables2 ? Object.keys(flow.variables2) : EMPTYARRAY;
			for (var key of arr)
				keys[key] = 1;
			var cl = [];
			for (var key in keys)
				cl.push({ id: '{' + key + '}', name: key });
			SET('#variables', cl);
		};

		// WebSocket messages
		ON('message', function(msg) {

			if (msg.callbackid) {
				var cb = flow.callbacks[msg.callbackid];
				if (cb) {
					delete flow.callbacks[msg.callbackid];
					if (msg.error)
						cb.callback([{ error: msg.error }]);
					else
						cb.callback(msg);
				}
			}

			switch (msg.TYPE) {

				case 'flow/flowstream':

					if (msg.name)
						document.title = msg.name;

					msg.proxyurl = location.origin;
					var tmp = (msg.proxypath || '');
					if (tmp)
						msg.proxyurl += common.root && tmp.charAt(0) === '/' ? (common.root + tmp.substring(1)) : tmp;

					SET('flow.head', msg);
					SET('flow.stats.paused', msg.paused == true);
					if (NAV.query.socket) {
						var socket = NAV.query.socket.replace(/^ws/, 'http');
						var index = socket.indexOf('/', 9);
						var origin = index === -1 ? socket : socket.substring(0, index);

						if (common.root)
							origin += common.root.substring(0, common.root.length - 1);

						if (origin && msg.origin !== origin)
							SETTER('websocket/send', { TYPE: 'origin', body: origin });
					}
					break;

				case 'flow/stats':

					if (common.page !== 'flow')
						return;

					SET('flow.stats', { messages: msg.messages, mm: msg.mm, pending: msg.pending, memory: msg.memory, paused: msg.paused });

					msg.traffic.priority.wait(function(key, next) {
						var sleep = 500;
						if (common.page === 'flow')
							Designer.CMD('flow.traffic', key, { delay: 100, count: msg.traffic[key], speed: 3, limit: 30 });
						setTimeout(next, sleep);
					});

					break;

				case 'flow/error':
					if (!common.console)
						SET('common.console', true);
					var com = msg.id ? flow.data[msg.id] : null;
					if (com) {
						if (flow.errors[msg.id])
							flow.errors[msg.id].push(msg);
						else
							flow.errors[msg.id] = [msg];
						UPD('flow.errors');
						W.flowinstances.exec(msg.id, 'error', msg);
					}

					PUSH('^common.logs.errors.items', { type: 'error', body: msg.ts.format('[ts]') + ' - ' + Thelpers.encode(msg.error) + (com ? ' <b>(' + com.name + ' component)</b>' : ''), id: msg.id });
					cleanlogs();

					break;

				case 'flow/call':
					var fn = flow.calls[msg.id];
					if (fn) {
						fn(msg.data);
						delete flow.calls[msg.id];
					}
					break;

				case 'flow/status':
					SET('flow.status.' + msg.id, msg.data || '');
					W.flowinstances.exec(msg.id, 'status', msg.data);
					setTimeout2('flow.refresh', () => Designer.CMD('flow.refresh'), 500, 2 * 8);
					break;

				case 'flow/redraw':

					var node = flow.data[msg.id];
					if (!node) {
						WARN('Instance {0} not found'.format(msg.id));
						return;
					}

					var com = flow.components.findItem('id', node.component);

					if (!msg.data.outputs && com)
						msg.data.outputs = com.outputs ? CLONE(com.outputs) : null;

					if (!msg.data.inputs && com)
						msg.data.inputs = com.inputs ? CLONE(com.inputs) : null;

					COPY(msg.data, flow.data[msg.id]);
					flow.config[msg.id] = msg.data.config;
					UPD('flow.data', 'update');
					UPD('flow.config.' + msg.id);
					Designer.CMD('flow.refresh');
					setTimeout(msg => W.flowinstances.exec(msg.id, 'redraw'), 100, msg);
					break;

				case 'flow/note':
					flow.data[msg.id].note = msg.data;
					SET('flow.note.' + msg.id, msg.data);
					if (common.page === 'flow')
						setTimeout(() => Designer.CMD('flow.refresh'), 100);
					W.flowinstances.exec(msg.id, 'note', msg.data);
					break;

				case 'flow/reset':
					SET('flow.errors', {});
					SET('common.logs.errors.items', []);
					$('.iserror').rclass('iserror');
					break;

				case 'flow/errors':

					for (var i = 0; i < msg.data.length; i++) {
						var err = msg.data[i];
						var com = err.id ? flow.data[err.id] : null;
						err.type = 'error';
						err.body = err.ts.format('[ts]') + ' - ' + Thelpers.encode(err.error) + (com ? ' <b>(' + com.name + ' component)</b>' : '');
						if (com) {
							if (flow.errors[err.id])
								flow.errors[err.id].push(err);
							else
								flow.errors[err.id] = [err];
							W.flowinstances.exec(err.id, 'error', err);
						}
					}

					UPD('flow.errors');

					if (msg.data.length) {
						PUSH('^common.logs.errors.items', msg.data);
						cleanlogs();
					}

					break;

				case 'flow/design':
					loadflow(msg.data);
					break;

				case 'flow/pause':
					var is = false;
					if (msg.is) {
						if (!flow.data.paused)
							flow.data.paused = {};
						flow.data.paused[msg.id] = 1;
						is = true;
					} else if (flow.data.paused) {
						delete flow.data.paused[msg.id];
						is = true;
					}

					is && UPD('flow.data', 1);
					break;

				case 'flow/move':
					var item = flow.data[msg.id];
					item.x = msg.data.x;
					item.y = msg.data.y;
					item.element.closest('.component').css({ left: item.x, top: item.y });
					W.flowinstances.exec(msg.id, 'move', msg.data);
					Designer.CMD('flow.refresh');
					break;

				case 'flow/groups':
					flow.data.groups = msg.data;
					Designer.CMD('flow.refresh');
					break;

				case 'flow/variables':
					SET('flow.variables', msg.data);
					W.flowinstances.exec('*', 'variables', msg.data);
					exports.refreshvariables();
					break;

				case 'flow/variables2':
					SET('flow.variables2', msg.data);
					W.flowinstances.exec('*', 'variables2', msg.data);
					exports.refreshvariables();
					break;

				case 'flow/notify':
					W.flowinstances.exec(msg.data.id || '*', 'notify', msg.data);
					break;

				case 'flow/components':
					loadcomponents(msg.data);
					break;

				case 'flow/config':
					if (msg.id && msg.data) {
						SET('flow.data.' + msg.id + '.config', msg.data);
						SET('flow.config.' + msg.id, msg.data);
						W.flowinstances.exec(msg.id, 'configure', msg.data);
					}
					break;
			}

		});

		exports.console = function() {
			TOGGLE('common.console');
		};

		ON('online', function(is) {
			if (is)
				SETTER('loading/hide', 1000);
			else
				SETTER('loading/show', '@(Connecting to the server...)');
		});

		exports.variables = function() {
			var model = {};
			model.variables = CLONE(flow.variables);
			model.callback = function(model) {
				SETTER('websocket/send', { TYPE: 'variables', data: model });
			};
			SET('variablesform @reset', model);
			SET('common.form', 'variablesform');
		};

		exports.component_contextmenu = function(el, e) {

			var id = ATTRD(el);
			var component = flow.components.findItem('id', id);

			var opt = {};
			opt.x = e.pageX;
			opt.y = e.pageY;
			opt.items = [];

			opt.items.push(component.name + (component.schema ? ' / <b>{schema}</b>'.arg(component) : ''));

			component.readme && opt.items.push({ id: 'readme', name: '@(Read information)', icon: 'ti ti-info-circle blue' });

			var meta = component.meta || EMPTYOBJECT;
			if (meta.secret)
				return;

			if (!meta.readonly && !TYPES[component.type]) {
				opt.items.push({ id: 'edit', name: '@(Edit)', icon: 'ti ti-laptop-code' });
				opt.items.push({ id: 'clone', name: '@(Clone)', icon: 'ti ti-clone' });
				opt.items.push({ id: 'copy2', name: '@(Copy source-code)', icon: 'ti ti-copy' });
				opt.items.push({ id: 'download', name: '@(Download)', icon: 'ti ti-save' });
				opt.items.push({ id: 'publish', name: '@(Publish to the community)', icon: 'ti ti-upload' });
			}

			if (!TYPES[component.type] && (!meta || meta.protected !== false)) {
				opt.items.push('-');
				opt.items.push({ id: 'remove', name: '@(Remove)', icon: 'ti ti-remove red' });
			}

			if (opt.items.length <= 1)
				return;

			opt.callback = function(item) {
				switch (item.id) {
					case 'readme':
						exports.scope();
						exports.readme(id);
						break;
					case 'edit':
					case 'clone':
					case 'copy':
					case 'copy2':
					case 'download':
						SETTER('websocket/send', { TYPE: 'component_read', id: id, callback: ASETTER('message/response', function(response) {

							response.body = response.data;
							response.data = null;

							if (item.id === 'download') {
								SETTER('filesaver/save', component.name + '.html', response.body);
								return;
							}

							if (item.id === 'copy' || item.id === 'copy2') {
								SETTER('clipboard/copy', item.id === 'copy2' ? response.body : ENCRYPT({ body: response.body }, common.secret, 'component'));
								SETTER('notify/success', '@(The component has been copied into the clipboard)');
								return;
							}

							if (item.id === 'clone')
								delete response.id;

							response.flowstreamid = flowstreamid;
							SET('componenteditor @reset', response);
							SET('common.page @hideloading', 'componenteditor');
						})});

						break;
					case 'remove':
						var com = exports.model.components.findItem('id', id);
						SETTER('approve/show', '@(Are you sure you want to remove selected the "{0}" component?)'.format(com.name), '"ti ti-remove" @(Remove)', ASETTER('websocket/send', { TYPE: 'component_remove', id: id }));
						break;
					case 'copycomponent':
						exports.copycomponents();
						break;
					case 'publish':
						SETTER('websocket/send', { TYPE: 'component_read', id: id, callback: ASETTER('message/response', function(response) {
							var data = {};
							data.type = 'component';
							data.name = component.name;
							data.email = CACHE('email') || '@';
							data.data = response.data;
							SET('publishform @reset', data);
							SET('common.form', 'publishform');
						})});
						break;
				}
			};
			SETTER('menu/show', opt);
		};

		exports.clear_sourcecache = function(id) {
			delete sourcecache[id];
		};

		exports.copy_components = function() {

			let selected = Designer.selected();
			if (!selected.components.length)
				return;

			let data = { type: 'copied_components', flowstreamid, items: [] };

			for (let com of selected.components) {
				const { id, component, name, config, inputs, outputs, connections, template, html, tab, x, y, $inputs, $outputs } = com.instance;
				data.items.push({ id, component, name, config, inputs, outputs, connections, template, html, tab, x, y, $inputs, $outputs });
			}

			SETTER('loading/hide', 500);
			SETTER('notify/success', '@(Selected components({0}) copied into the clipboard.)'.format(data.items.length));
			SETTER('clipboard/copy', STRINGIFY(data));

		};

		exports.export_components = function() {

			var selected = Designer.selected();

			if (!selected.components.length)
				return;

			var data = { type: 'exported_components', flowstreamid, items: [], components: {}};
			var components = {};

			for (var com of flow.components)
				components[com.id] = com.name;

			for (var com of selected.components) {
				var { id, component, name, config, inputs, outputs, connections, tab, x, y, $inputs, $outputs } = com.instance;
				data.items.push({ id, component, name, config, inputs, outputs, connections, tab, x, y, $inputs, $outputs });
			}

			SETTER('loading/show', '@(Loading source(s) from server)');

			data.items.wait(function(item, next){

				if (data.components[item.component]) {
					next();
					return;
				}

				SETTER('websocket/send', { TYPE: 'component_read', id: item.component, callback: ASETTER('message/response', function(response) {
					data.components[item.component] = { name: components[item.component], source: response.data };
					sourcecache[item.id] = response.data;
					next();
				})});

			}, function done(){
				SETTER('loading/hide', 500);
				SETTER('notify/success', '@(Selected components({0}) copied into the clipboard.)'.format(data.items.length));
				SETTER('clipboard/copy', STRINGIFY(data));
			});
		};

		exports.paste_components = function(pos) {

			navigator.clipboard.readText().then(function(text) {

				var data = PARSE(text);
				if (!data) {
					SETTER('notify/warning', '@(Invalid data)');
					return;
				}

				if ((data.type === 'copied_components' || data.type === 'exported_components') && data.flowstreamid === flowstreamid) {
					paste_components(data.items, pos);
					return;
				} if (data.type === 'copied_components' && data.flowstreamid !== flowstreamid) { // external copy & local paste
					SETTER('notify/warning', '@(Please use "Export" menu to copy between FlowStream instances.)');
					return;
				} else if (data.type === 'exported_components') { // external copy&paste

					SETTER('loading/show');

					var isinstall = false;
					var names = {};

					for (var com of flow.components)
						names[com.name] = com.id;

					Object.keys(data.components).wait(function(comid, next) {

						// check if we know this component
						var com = data.components[comid];
						var tmp = names[com.name];

						var updateid = function(id) {
							for (var item of data.items) {
								if (item.component === comid)
									item.component = id;
							}
							next();
						};

						if (tmp) {
							updateid(tmp);
							return;
						}

						install_component(com.source, function(id) {
							com.id = id;
							isinstall = true;
							updateid(id);
						});

					}, function() {
						var done = function() {
							SETTER('loading/show', '<i class="ti ti-check"></i>@(Import finished)');
							paste_components(data.items, pos);
							SETTER('loading/hide', 1000);
						};
						if (isinstall)
							WATCHONCE('flow.data', done);
						else
							done();
					});

					return;

				} else if (data.type === 'exported_as_component' && data.component) {
					SETTER('approve/show', '@(Exported flow will be imported as "{0}" component.)'.format(data.name), '"file-import" @(Import)', function(){
						var id = FUNC.makeid('c');
						var body = FUNC.rtrim(data.component);
						SETTER('websocket/send @showloading', { TYPE: 'component_save', id: id, data: body, callback: function(){
							SET('common.page', 'flow');
							SETTER('loading/hide');
							setTimeout(() => EMIT('resize'), 100);
						}});
					});
					return;
				}

				SETTER('notify/warning', '@(Invalid data)');

			}).catch(err => console.error(err));
		};

		function fix_positions(arr, pos) {
			let minx, miny;

			if (!pos.x && !pos.y) {
				var scroll = Designer.element.closest('.ui-scrollbar-area');
				var num = (Math.random() * 100) >> 0;
				pos.x = scroll.scrollLeft() + num + 200;
				pos.y = scroll.scrollTop() + num + 200;
			}

			// find minx,miny and set it as zero point offset
			for (let item of arr) {
				(!minx || minx > item.x) && (minx = item.x);
				(!miny || miny > item.y) && (miny = item.y);
			}

			// offset each component based on cursor position(pos)
			for (let item of arr) {
				item.x = item.x - minx + pos.x;
				item.y = item.y - miny + pos.y;
			}

		}

		function paste_components(items, pos) {

			var model = exports.model;
			var add = [];
			var cloned = {};
			var now = Date.now();

			fix_positions(items, pos);

			for (var item of items) {
				var com = model.components.findItem('id', item.component);
				var newbie = {};
				newbie.id = 'i' + now.toString(36);
				newbie.x = item.x;
				newbie.y = item.y;
				newbie.changes = { newbie: true };
				newbie.name = item.name;
				newbie.component = item.component;
				newbie.config = CLONE(item.config);
				newbie.connected = false;
				newbie.connections = CLONE(item.connections);
				newbie.name = item.name.slug().replace(/\-/g, '');
				newbie.$outputs = item.$outputs;
				newbie.outputs = item.outputs;
				newbie.$inputs = item.$inputs;
				newbie.inputs = item.inputs;
				newbie.onmake = onmake;
				newbie.onremove = onremove;
				newbie.tab = model.tab;
				newbie.html = '<figure>' + com.html.format(newbie.id) + '</figure>';
				newbie.template = com.template ? Tangular.compile(com.template) : null;
				cloned[item.id] = newbie.id;
				add.push(newbie);
				SET('flow.note.' + newbie.id, model.note[item.id]);
				SET('flow.config.' + newbie.id, newbie.config);
				now++;
			}

			for (var item of add) {
				for (var key in item.connections) {
					for (var output of item.connections[key]) {
						if (cloned[output.id])
							output.id = cloned[output.id];
					}
				}
				model.data[item.id] = item;
			}

			Designer.find('.component-focused').rclass('component-focused');
			Designer.op.unselect();

			UPD('?.data', 'update');
			SET('?.changed', true);

			setTimeout(function(add) {
				for (var item of add)
					Designer.op.select(item.id, true);
			}, 500, add);
		}

		function install_component(str, callback) {

			var name;
			var id;

			var m = str.match(/exports\.id(\s=).*?;/);
			if (m) {
				try {
					id = new Function('exports', 'return ' + m[0])({});
				} catch (e) {}
			}

			if (!id)
				id = FUNC.makeid('c');

			m = str.match(/exports\.name(\s=).*?;/);
			if (m) {
				try {
					name = new Function('exports', 'return ' + m[0])({});
				} catch (e) {
					// Error
					callback();
					return;
				}
			}

			SETTER('websocket/send', { TYPE: 'component_save', id: id, data: FUNC.rtrim(str), callback: function() {
				callback(id, name);
			}});
		}

		exports.selected_settings = function() {
			var info = exports.data.info;
			if (info.type === 'group')
				exports.groupedit(info.selected);
			else
				exports.settings(info.selected);
		};

		exports.selected_remove = function() {
			Designer.CMD('flow.selected.clear');
		};

		exports.info = function() {

			var model = exports.data;

			if (model.info.type === 'component') {
				exports.readme(model.info.selected.component);
				return;
			}

			var head = model.head;
			var body = '';

			if (head.version2)
				body += '| @(Version) | __' + head.version2 + '__ |\n';

			if (head.author)
				body += '| @(Author) | __' + head.author + '__ |\n';

			if (head.url && head.url.length > 5)
				body += '| @(Website) | <' + head.url + '> |\n';

			body += (body ? '\n' : '') + (head.readme || '');
			FUNC.readme(head.name, body);
		};

		exports.notewindow = function(el) {
			var id = ATTRD(el);
			var note = flow.note[id];
			var instance = flow.data[id];
			note && FUNC.readme(instance.Component.name, note);
		};

		exports.copyconfig = function(el) {
			SETTER('clipboard/copy', STRINGIFY({ type: 'config', componentid: flow.selected.component, componentname: flow.selected.name, data: CLONE(flow.config[flow.selected.id]) }));
			SETTER('notify/success', '@(The configuration copied into clipboard.)');
		};

		exports.pasteconfig = function(el) {
			navigator.clipboard.readText().then(function(text) {
				var data = PARSE(text);
				if (data && data.type === 'config' && flow.selected.name === data.componentname) {
					SET('?.settings.settings_f' + flow.selected.component, data.data);
					CHANGE('?.settings.settings_f' + flow.selected.component + '.*', true);
					SETTER('notify/success', '@(Config pasted from clipboard.)');
				} else
					SETTER('notify/warning', '@(Incompatible config)');
			}).catch(err => console.error(err));
		};

		exports.resetconfig = function(el) {
			SET('?.settings.settings_f' + flow.selected.component, {});
			CHANGE('?.settings.settings_f' + flow.selected.component + '.*', true);
			SETTER('notify/success', '@(Config reset.)');
		};

		FIND('flow', function(com) {
			var zoom = FUNC.pref_read('zoom');
			Designer.CMD('flow.zoom', zoom);
		});

		FIND('#flowviewbox', function(com) {
			com.scrollbar.options.onidle = function() {
				FUNC.pref_save('scroll', { x: com.scrollbar.scrollLeft(), y: com.scrollbar.scrollTop() });
			};
			var scroll = FUNC.pref_read('scroll');
			scroll && com.scrollbar.scroll(scroll.x, scroll.y);
		});

		WATCH('?.info', function(path, value) {
			FUNC.pref_save('zoom', (value.zoom || 100).toFixed(1).parseFloat());
		});

		exports.init();
	});

	function htmldecode(text) {
		switch (text) {
			case '&gt;':
				return '>';
			case '&lt;':
				return '<';
			case '&amp;':
				return '&';
			case '&quot;':
				return '"';
			case '&#39;':
				return '\'';
		}
		return text;
	}

	FUNC.reconfigure = function(el, config) {
		var id = (el instanceof jQuery ? el.attrd2('id') : el) || '';
		var instance = flow.data[id];
		if (instance) {
			if (instance.connected) {
				SETTER('websocket/send', { TYPE: 'reconfigure', id: id, data: config });
				SETTER('notify/success', '@(The configuration has been applied successfully)');
			} else {
				instance.config = config;
				UPD('flow.data.' + id + '.config');
				SET('flow.config.' + id, config);
				W.flowinstances.exec(instance.id, 'configure', config);
				Designer.CMD('flow.refresh');
			}
		}
	};

	ON('markdown', function(el) {
		var arr = el[0].tagName === 'BODY' ? el.find('.markdown pre code') : el.find('pre code');
		for (var t of arr) {
			if (!t.$mdcodemirror) {
				t.$mdcodemirror = true;
				var el = $(t);
				var type = 'application/ld+json';

				if (el.hclass('lang-js'))
					type = 'text/javascript';
				else if (el.hclass('lang-html'))
					type = 'text/html';
				else if (el.hclass('lang-sql'))
					type = 'text/x-sql';

				el.aclass('cm-s-default');
				var code = FUNC.strim(el.html()).trim().replace(/&gt;|&lt;|&quot;|&#39;|&amp;/g, htmldecode);
				CodeMirror.runMode(code.replace(/\t/g, '  '), type, el[0]);
			}
		}
	});

	isTOUCH && (function() {

		var delay;

		$(document).on('touchstart touchmove touchend', '.component,.connection,.ui-flow-group', function(e) {

			if (e.type === 'touchstart') {
				delay && clearTimeout(delay);
				delay = setTimeout(function(touch) {
					delay = null;
					EXEC('flow/contextmenu', { pageX: touch.pageX, pageY: touch.pageY });
				}, 2000, e.touches[0]);
				return;
			}

			delay && clearTimeout(delay);
			delay = null;
		});

	})();

	Thelpers.url = function(val) {
		var index = val.indexOf('/', 10);
		return index === -1 ? val : val.substring(0, index);
	};

	FUNC.trigger = function(el, data) {

		if (flow.changed) {
			SETTER('message/warning', '@(While the FlowStream contains unapplied changes, this action is not permitted.)');
			return;
		}

		if (data && data.constructor !== Object)
			data = null;
		var isel = el instanceof jQuery;
		if (!data || typeof(data) !== 'object')
			data = isel ? { data: el.attrd('data') } : {};
		setTimeout(function(id, data) {
			if (id && flow.data[id] && flow.data[id].connected) {
				data.TYPE = 'trigger';
				data.id = id;
				SETTER('websocket/send', data);
			}
		}, 10, isel ? el.attrd2('id') : el, data);
		return data;
	};

</script>